// SPDX-FileCopyrightText: 2019-2023 Giovanni Dante Grazioli <deroad@libero.it>
// SPDX-License-Identifier: BSD-3-Clause

// https://www.rockbox.org/wiki/pub/Main/DataSheets/sh1_2p.pdf
// http://www.shared-ptr.com/sh_insns.html


import Instruction from '../core/instruction.js';
import Base from '../core/base.js';
import Variable from '../core/variable.js';

const _s = 's';
const _carry = 'c';
const _mac = 'mac';

const _size_map = {
	"b": 8,
	"w": 16,
	"l": 32
};

const _compare_map = {
	'eq': 'EQ',
	'hi': 'GT',
	'hs': 'GE',
	'ge': 'GE',
	'gt': 'GT',
	'pl': 'GT',
	'pz': 'GE',
	'str': 'str',
};

const _reverse_map = {
	'EQ': 'NE',
	'GT': 'LE',
	'GE': 'LT',
};

const _branch_list = [
	'bra',
	'braf',
	'bsr',
	'bsrf',
	'jmp',
	'jsr',
	'rte',
	'rts'
];

function _set_conditional(instr, context, cmp, invert) {
	if (invert) {
		cmp = _reverse_map[cmp];
	}
	instr.conditional(context.cond.a, context.cond.b, cmp);
	return Base.nop();
}

function _require_composed(instr, idx) {
	var e = instr.parsed;
	return e.opd[idx].increment || e.opd[idx].decrement;
}

function _handle_pointer(instr, idx, ops) {
	var e = instr.parsed;
	var size = e.size || 32;
	if (e.opd[idx].increment) {
		ops.push(Base.increase(e.opd[idx].token, size / 8));
	} else if (e.opd[idx].decrement) {
		ops.unshift(Base.decrease(e.opd[idx].token, size / 8));
	}
}

function _arg(instr, idx) {
	var e = instr.parsed;
	if (e.opd[idx].pointer) {
		// a great way to fix PC + N
		if (e.opd[idx].token.startsWith('pc +')) {
			var n = parseInt(e.opd[idx].token.substr('pc +'.length));
			var p = instr.location.add(n);
			e.opd[idx].token = '0x' + p.toString(16);
		}
		return Variable.pointer(e.opd[idx].toString(), e.size || 32, true);
	}
	return e.opd[idx].toString();
}

function _op_move(instr) {
	var ops = [];
	var dst = _arg(instr, 0);
	var src = _arg(instr, 1);
	if (instr.parsed.size) {
		ops.push(Base.cast(dst, src, 'int' + instr.parsed.size + '_t'));
	} else {
		ops.push(Base.assign(dst, src));
	}
	if (_require_composed(instr, 0) || _require_composed(instr, 1)) {
		if (_require_composed(instr, 0)) {
			_handle_pointer(instr, 0, ops);
		} else {
			_handle_pointer(instr, 1, ops);
		}
		return Base.composed(ops);
	}
	return ops[0];
}

var _is_last_instruction = function(instr, instructions) {
	return instructions.indexOf(instr) == (instructions.length - 1);
};

export default {
	instructions: {
		mov: _op_move,
		mova: _op_move,
		movt: function(instr, context, instructions) {
			instr.parsed.opd[0].token = _carry;
			return _op_move(instr);
		},
		swap: function(instr, context, instructions) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.swap(dst, src, 32);
		},
		/*
		xtrct: function(instr, context, instructions) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.?????(dst, src);
		},
		 */
		add: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.add(dst, dst, src);
		},
		addc: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.add(dst, dst, src + ' + ' + _carry);
		},
		addv: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			instr.comments.push('if overflows, then ' + _carry + ' = 1.');
			return Base.add(dst, dst, src + ' + ' + _carry);
		},
		cmp: function(instr, context) {
			var a = _arg(instr, 0);
			var b = _arg(instr, 1);
			if (instr.parsed.cmp != 'str') {
				context.cond.a = a;
				context.cond.b = b;
				context.cond.cmp = instr.parsed.cmp;
			} else {
				context.cond.a = (a === b) ? a : '(' + a + ' & ' + b + ')';
				context.cond.b = '0';
				context.cond.cmp = 'EQ';
			}
		},
		tst: function(instr, context) {
			var a = _arg(instr, 0);
			var b = _arg(instr, 1);
			context.cond.a = (a === b) ? a : '(' + a + ' & ' + b + ')';
			context.cond.b = '0';
			context.cond.cmp = 'EQ';
		},
		div1: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.divide(dst, dst, src);
		},
		dmuls: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.multiply(dst, dst, src);
		},
		dt: function(instr) {
			var dst = _arg(instr, 1);
			return Base.decrease(dst, dst, 1);
		},
		exts: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.cast(dst, src, 'int' + instr.parsed.size + '_t');
		},
		extu: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.cast(dst, src, 'uint' + instr.parsed.size + '_t');
		},
		mul: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.multiply(_mac, dst, src);
		},
		muls: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.multiply(_mac, dst, src);
		},
		mulu: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.multiply(_mac, dst, src);
		},
		neg: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.negate(dst, src);
		},
		negc: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.composed([
				Base.negate(dst, src),
				Base.subtract(dst, dst, _carry),
			]);
		},
		sub: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.subtract(dst, dst, src);
		},
		subc: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.composed([
				Base.subtract(dst, dst, src),
				Base.subtract(dst, dst, _carry),
			]);
		},
		subv: function(instr) {
			instr.comments.push('if underflows, then ' + _carry + ' = 1.');
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.subtract(dst, dst, src);
		},
		and: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.and(dst, dst, src);
		},
		not: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.not(dst, src);
		},
		or: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.or(dst, dst, src);
		},
		xor: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.xor(dst, dst, src);
		},
		rotl: function(instr) {
			var src = _arg(instr, 1);
			return Base.rotate_left(src, src, 1, 32);
		},
		rotr: function(instr) {
			var src = _arg(instr, 1);
			return Base.rotate_right(src, src, 1, 32);
		},
		rotcl: function(instr) {
			var src = _arg(instr, 1);
			return Base.rotate_left(src, src, 1, 32);
		},
		rotcr: function(instr) {
			var src = _arg(instr, 1);
			return Base.rotate_right(src, src, 1, 32);
		},
		shal: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_left(src, src, 1);
		},
		shar: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_right(src, src, 1);
		},
		shll: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_left(src, src, 1);
		},
		shlr: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_right(src, src, 1);
		},
		shll2: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_left(src, src, 2);
		},
		shlr2: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_right(src, src, 2);
		},
		shll8: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_left(src, src, 8);
		},
		shlr8: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_right(src, src, 8);
		},
		shll16: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_left(src, src, 16);
		},
		shlr16: function(instr) {
			var src = _arg(instr, 1);
			return Base.shift_right(src, src, 16);
		},
		ldc: _op_move,
		lds: _op_move,
		stc: _op_move,
		sts: _op_move,
		clrt: function(instr) {
			return Base.assign(_carry, '0');
		},
		clrs: function(instr) {
			return Base.assign(_s, '0');
		},
		sett: function(instr) {
			return Base.assign(_carry, '1');
		},
		sets: function(instr) {
			return Base.assign(_s, '1');
		},
		fmov: _op_move,
		flds: _op_move,
		fsts: _op_move,
		fldi0: function(instr) {
			var src = _arg(instr, 1);
			return Base.assign(src, '0.0');
		},
		fldi1: function(instr) {
			var src = _arg(instr, 1);
			return Base.assign(src, '1.0');
		},
		fabs: function(instr) {
			var src = _arg(instr, 1);
			return Base.and(src, src, '0x7FFFFFFF');
		},
		fneg: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.negate(dst, src);
		},
		fadd: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.add(dst, dst, src);
		},
		fsub: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.subtract(dst, dst, src);
		},
		fmul: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.multiply(dst, dst, src);
		},
		fdiv: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.divide(dst, dst, src);
		},
		fsqrt: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.assign(dst, Base.call('sqrt', [src]));
		},
		fcmp: function(instr, context) {
			var a = _arg(instr, 0);
			var b = _arg(instr, 1);
			context.cond.a = a;
			context.cond.b = b;
			context.cond.cmp = instr.parsed.cmp;
		},
		float: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.cast(dst, src, 'float');
		},
		ftrc: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.cast(dst, src, 'int32_t');
		},
		fsrra: function(instr) {
			var dst = _arg(instr, 0);
			var src = _arg(instr, 1);
			return Base.special(dst + ' = 1.0 / sqrt (' + src + ')');
		},
		fsca: function(instr) {
			var dst1 = _arg(instr, 0);
			var dst2 = 'fr' + (parseInt(dst1.substr(2)) + 1).toString(16);
			var src = _arg(instr, 1);
			return Base.composed([
				Base.assign(dst1, Base.call('sin', [src])),
				Base.assign(dst2, Base.call('cos', [src]))
			]);
		},
		bt: function(instr, context, instructions) {
			return _set_conditional(instr, context, 'EQ');
		},
		bf: function(instr, context, instructions) {
			return _set_conditional(instr, context, 'NE');
		},
		jsr: function(instr, context, instructions) {
			var e = instr.parsed.opd;
			return Base.call(Variable.functionPointer(e[1].token), []);
		},
		bsr: function(instr, context, instructions) {
			var e = instr.parsed.opd;
			if (_is_last_instruction(instr, instructions)) {
				return Base.call(Variable.functionPointer(e[1].token), []);
			}
			return Base.nop();
		},
		bra: function(instr) {
			var callname = _arg(instr, 1);
			if (callname.startsWith('0x')) {
				callname = Variable.functionPointer(callname);
			}
			return Base.call(callname, []);
		},
		braf: function(instr) {
			var src = _arg(instr, 1);
			return Base.call(Variable.functionPointer(src), []);
		},
		rte: function() {
			return Base.return();
		},
		rts: function() {
			return Base.return();
		},
		nop: function() {
			return Base.nop();
		},
		invalid: function(instr, context, instructions) {
			return Base.nop();
		}
	},
	parse: function(assembly) {
		assembly = assembly.trim().toLowerCase();
		assembly = assembly.replace(/,/g, ' ');
		assembly = assembly.replace(/\s+/g, ' ');

		/*
		   Regex explaination:
			^
			  (\w+)
			  \.?
			  ([wlb])? // word/long/byte
			  ([sn])?  // delayed branch
			  \/?
			  (hi|eq|hs|pl|pz|str)? // compare flags
			   ?       // space might not exists (aka no 1st and 2nd param)
			  (
				(
					(@)? // Indirect or Direct access
					(
						(\(\w+\s\w+\)) A + B value
						  |
							(-)?  Pre-decrement
							([\w._]+) register
							(\+)? Post-increment
					)
				)
			  )?
			   ?       // space might not exists (aka no 2nd param)
			  (
				(
					(@)? // Indirect or Direct access
					(
						(\(\w+\s\w+\)) A + B value
						  |
							(-)?  Pre-decrement
							([\w._]+) register
							(\+)? Post-increment
					)
				)
			  )?
			$
		*/
		var tokens = assembly.match(/^(\w+)\.?([wlb])?([sn])?\/?(hi|eq|hs|pl|pz|str)? ?(((@)?((\(\w+\s\w+\))|(-)?([\w._]+)(\+)?)))? ?(((@)?((\(\w+\s\w+\))|(-)?([\w._]+)(\+)?)))?$/);
		var mnem = tokens[1];
		var size = _size_map[tokens[2]];
		var delayed = tokens[3] == 's';
		var cmp = _compare_map[tokens[4]];
		var opd = [{
			pointer: tokens[7] == '@',
			decrement: tokens[10] == '-',
			increment: tokens[12] == '+',
			token: tokens[11] || (tokens[9] ? tokens[9].replace(/\((\w+)\s(\w+)\)/g, '$2 + $1') : null),
			toString: function() {
				return this.token || '(null)';
			}
		}, {
			pointer: tokens[15] == '@',
			decrement: tokens[18] == '-',
			increment: tokens[20] == '+',
			token: tokens[19] || (tokens[17] ? tokens[17].replace(/\((\w+)\s(\w+)\)/g, '$2 + $1') : null),
			toString: function() {
				return this.token || '(null)';
			}
		}];

		return {
			mnem: mnem,
			delayed: delayed,
			cmp: cmp,
			size: size,
			opd: opd.reverse()
		};
	},
	context: function() {
		return {
			cond: {
				cmp: null,
				a: '?',
				b: '?'
			}
		};
	},
	preanalisys: function(instructions, context) {
		/* delayed branch fix */
		for (var i = 0; i < (instructions.length - 1); i++) {
			var op = instructions[i].parsed.mnem;
			if (_branch_list.indexOf(op) >= 0 && instructions[i + 1].parsed.mnem != 'nop') {
				Instruction.swap(instructions, i, i + 1);
				++i;
			} else if (instructions[i].parsed.delayed && instructions[i + 1].parsed.mnem != 'nop') {
				Instruction.swap(instructions, i, i + 1);
				++i;
			}
		}
	},
	postanalisys: function(instructions, context) {},
	localvars: function(context) {
		return [];
	},
	globalvars: function(context) {
		return [];
	},
	arguments: function(context) {
		return [];
	},
	returns: function(context) {
		return 'void';
	}
};
